/*
can only have zero D' if resultList is empty
resultList only empty if no results for test
no results for test only if not loaded correctly
 */

package simorg;

import java.sql.SQLException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Random;
import java.util.Set;

import com.sun.net.ssl.internal.ssl.Debug;

import simorg.Log.DebugLevel;

public class Agent {

	private enum Mode {
		INCREMENT, RANDOM
	}

	public static void main(final String a_args[]) {

		// initialize the log
		Log.initialize();

		Log.writeLine("Testing Agent.");

		// create the agent
		final Agent agent = new Agent();

		// run the agent
		try {
			agent.run();
		} catch (final RuntimeException rtex) {
			rtex.printStackTrace();
		}

		System.out.println("Done.");
		Log.flush();
	}

	private final Set<String> m_activeClassSet = new LinkedHashSet<String>();

	private final ArrayList<Test> m_activeTestsList = new ArrayList<Test>();

	private final ArrayList<Unknown> m_activeUnknownsList = new ArrayList<Unknown>();

	private final DatabaseInterface m_dbInterface = DatabaseInterface.getSingleton();

	private final DecimalFormat m_formatter = new DecimalFormat("0.00###");

	private final DecimalFormat m_shortFormatter = new DecimalFormat("0.0");

	private final Mode m_mode = Mode.INCREMENT;

	private String m_resultsTable = "";

	private float m_timePenaltyMin;

	private float m_timePenaltyMax;

	private Unknown m_selectedUnknown;

	private final float[] WEIGHT = { 0.65f, 0.8f, 0.87f, 0.95f, 1.0f };

	private final float BASE_TIME_PENALTY = 0.1f;

	private final int BP_MP_DOWN_RANGE = -6;

	private final int BP_MP_UP_RANGE = 8;
	
	private final float DERIV_DF_DOWN_RANGE = -1.0f;
	
	private final float DERIV_DF_UP_RANGE = 3.0f;

	// the number of unknowns to increment by each run through
	private final int UNKNOWN_INCREMENT = 1;

	public Agent() {

		// load everything from the database
		try {
			m_dbInterface.loadAll();
		} catch (final SQLException ex) {
			ex.printStackTrace();
		}

		initialize();
	}

	/**
	 * Calculate the Classification Factor which represents the relative number
	 * of classes that will be removed if the given test is successful.
	 * 
	 * @param a_test
	 *            The test to use as the basis for the CF.
	 * @return The CF.
	 */
	public float calculateClassFactor(final Test a_test) {

		//if(m_selectedUnknown.getResult(a_test) == null) return 0;
		
		// the number of classes remaining before the test is run
		final int numActiveClass = m_activeClassSet.size();

		// results from running the given test
		final ArrayList<TestResult> resultList = getTestResults(a_test);

		// unique results
		final Set<TestResult> uniqueResultSet = new LinkedHashSet<TestResult>(
				resultList);

		float classFactor = 0.0f;

		// for every result...
		for (final TestResult result : uniqueResultSet) {

			// the number of results that match this one
			final int numUnknownsWithMatchingResult = Collections.frequency(resultList,
					result);

			// get the unique classes remaining should this result be correct
			final Set<String> remainingClassSet = new LinkedHashSet<String>();
			for (final Unknown unknown : m_activeUnknownsList) {
				final TestResult otherResult = unknown.getResult(a_test);

				// if the result for the current test on the current unknown matches,
				// the unknown's class remains after running the test
				if (result == otherResult) {
					remainingClassSet.add(unknown.getUnknownClass());
				}
			}

			final int numClassElim = m_activeClassSet.size()
					- remainingClassSet.size();
			final int numActiveUnknowns = m_activeUnknownsList.size();

			// CF += (Num Classes Eliminated / Num Active Classes)
			// * (Num Unknowns with this Result / Num Active Unknowns)
			classFactor += ((float) numClassElim / (float) numActiveClass)
				* ((float) numUnknownsWithMatchingResult / (float) numActiveUnknowns);
		}

		return classFactor;
	}

	/**
	 * Calculate the Discrimination Factor which represents the relative number
	 * of unknowns that will be removed if the given test is successful.
	 * 
	 * @param a_test
	 *            The test to use as the basis for the DF.
	 * @return The DF.
	 */
	public float calculateDiscrimFactor(final Test a_test,
			final float a_downRange, final float a_upRange) {
		
		//if(m_selectedUnknown.getResult(a_test) == null) return 0;

		// for each unknown, get the result of running this test
		final ArrayList<TestResult> resultList = getTestResults(a_test);

		// unique results (don't need this?)
		final Set<TestResult> uniqueResultSet = new LinkedHashSet<TestResult>(
				resultList);

		// get all the results including ranges
		final ArrayList<TestResult> expandedResultList = new ArrayList<TestResult>();

		for (final TestResult curResult : resultList) {
			
			// don't expand negative results or results with extra "stuff" (d, d., (di))
			if(curResult.getResult() == Unknown.NEGATIVE_RESULT 
					&& curResult.getExtra() != TestResult.ExtraType.NONE) {
				expandedResultList.add(curResult);
				continue;
			}
			
			// create individual results around the range
			for (int mod = (int) a_downRange; mod <= a_upRange; mod++) {
				TestResult newResult = new TestResult(curResult);
				newResult.setResult(newResult.getResult() + mod);
				expandedResultList.add(newResult);
			}
		}

		// limit to the unique expanded results
		final Set<TestResult> uniqueExpandedResultSet 
			= new LinkedHashSet<TestResult>(expandedResultList);

		float discrimFactor = 0.0f;
		final int numActiveUnknowns = m_activeUnknownsList.size();
		final float rangeSize = a_upRange - a_downRange + 1;
		int numEliminated = 0;
		float rangeFactor = 0.0f;

		for (final TestResult uniqueExpandedResult : uniqueExpandedResultSet) {
			int numRemaining = 0;
			int frequency = 0;
			
			for (final TestResult uniqueResult : uniqueResultSet) {

				if(uniqueExpandedResult.equals(uniqueResult)) {
					rangeFactor = 0.0f;
				} else if(uniqueExpandedResult.getResult() == Unknown.NEGATIVE_RESULT 
						|| uniqueExpandedResult.getExtra() != TestResult.ExtraType.NONE ) {
					rangeFactor = rangeSize;
				} else if(uniqueResult.getResult() == Unknown.NEGATIVE_RESULT
						|| uniqueResult.getExtra() != TestResult.ExtraType.NONE ) {
					rangeFactor = 0.0f;
				} else {
					
					// check whether the other result is in range of the current result
					final float minResult = uniqueResult.getResult() + a_downRange;
					final float maxResult = uniqueResult.getResult() + a_upRange;
					if (uniqueExpandedResult.getResult() >= minResult
							&& uniqueExpandedResult.getResult() <= maxResult) {
						rangeFactor = 1.0f;
	
						// get the frequency of this result
						frequency += Collections.frequency(resultList, uniqueResult.getResult());
						numEliminated = resultList.size() - frequency;
					}
				}

				numRemaining = Collections.frequency(resultList, uniqueResult);

				if (rangeFactor > 0.0f) {
					// TestDF += (myResult.Eliminated /
					// (float)ActiveUnknowns.Count())
					// * (isInRange / rangeSize * numRemainingResults /
					// ActiveUnknowns.Count());
					discrimFactor += ((float) numEliminated / (float) numActiveUnknowns)
							* ((float)rangeFactor / (float)rangeSize * (float)numRemaining / 
									(float)numActiveUnknowns);
				}
			}

			numEliminated = resultList.size() - frequency;
		}

		return discrimFactor;
	}

	/**
	 * Calculate max and min time penalties based on the current list of tests.
	 */
	private void calculateTimePenalties() {
		int minTimePenalty = Integer.MAX_VALUE;
		int maxTimePenalty = Integer.MIN_VALUE;

		for (final Test test : m_activeTestsList) {
			final int testTime = test.getTimeMinutes();

			if (testTime < minTimePenalty) {
				minTimePenalty = testTime;
			}

			if (testTime > maxTimePenalty) {
				maxTimePenalty = testTime;
			}
		}

		m_timePenaltyMin = minTimePenalty;
		m_timePenaltyMax = maxTimePenalty;

		Log.writeLine("Min time penalty: " + minTimePenalty
				+ "\nMax time penalty: " + maxTimePenalty, DebugLevel.MORE);
	}

	/**
	 * Convert a factor (DF or CF) into a score by involving weight factor and
	 * time penalty.
	 * 
	 * @param a_test
	 *            The test to use as a basis for the score.
	 * @param a_factor
	 *            The base DF or CF.
	 */
	public float convertToScore(final Test a_test, final float a_factor) {

		// if the test is null, return the lowest (greatest negative) possible
		// score
		if (a_test == null) {
			return Float.NEGATIVE_INFINITY;
		}

		// calculate weighting factor
		final float weightFactor = WEIGHT[a_test.getWeightIndex()];
		a_test.setWeightFactor(weightFactor);

		final float timePenalty = getTimePenalty(a_test);
		a_test.setTimePenalty(timePenalty);

		final float score = (a_factor * weightFactor) - timePenalty;

		// score = max(score, 0)
		return score > 0 ? score : 0;
	}

	/**
	 * Get test results for the given test on all active unknowns.
	 * 
	 * @param a_test
	 *            The test whose results you want to get.
	 * @return A list of the results for the given test.
	 */
	private ArrayList<TestResult> getTestResults(final Test a_test) {
		final ArrayList<TestResult> resultList = new ArrayList<TestResult>();

		for (final Unknown unknown : m_activeUnknownsList) {
			final TestResult result = unknown.getResult(a_test);

			if (result != null) {
				resultList.add(result);
			}
		}

		// debug display
		/*
		 * Log.write("Results: "); for(TestResult result : resultList) {
		 * Log.write(result.getStringId() + " "); } Log.writeLine();
		 */

		return resultList;
	}

	/**
	 * Gets the time penalty for the given test which affects the total score
	 * based on the amount of time required to run the given test.
	 * 
	 * @param a_test
	 *            The test whose time penalty we wish to calculate.
	 * @return The time penalty.
	 */
	public float getTimePenalty(final Test a_test) {
		final float timePenalty = ((a_test.getTimeMinutes() - m_timePenaltyMin) / (m_timePenaltyMax - m_timePenaltyMin))
				* BASE_TIME_PENALTY;

		return timePenalty;
	}

	/**
	 * Initialize the agent with the full list of tests, unknowns, and classes.
	 * Set time penalties accordingly. Select an unknown based on the current
	 * mode.
	 */
	private boolean initialize() {
		loadTests();
		loadUnknowns();
		calculateTimePenalties();

		return selectUnknown();
	}

	/**
	 * Load all tests into the agent.
	 */
	private void loadTests() {
		Log.writeLine("[ Loading tests into agent ].", Log.DebugLevel.MOST);

		// clear tests in case we're re-initializing
		m_activeTestsList.clear();

		// load up all the tests as active
		for (final Test test : m_dbInterface.getAllTests()) {
			m_activeTestsList.add(test);
		}

		Log.writeLine("Loaded " + m_activeTestsList.size() + " tests.");
	}

	/**
	 * Load all unknowns into the agent.
	 */
	private void loadUnknowns() {
		Log.writeLine("[ Loading unknowns into agent. ]", Log.DebugLevel.MOST);

		// clear unknowns in case we're re-initializing
		m_activeUnknownsList.clear();

		// load up all the unknowns as active
		for (final Unknown unknown : m_dbInterface.getAllUnknowns()) {
			m_activeUnknownsList.add(unknown);
		}

		Log.writeLine("Loaded " + m_activeUnknownsList.size() + " unknowns.");
	}

	/**
	 * Remove all unknowns outside the acceptable range of boiling points.
	 */
	public void removeOutsideBoilingPointRange() {
		if (!m_selectedUnknown.hasBoilingPoint()) {
			return;
		}

		final int boilingPoint = m_selectedUnknown.getBoilingPoint();
		final int minBoilingPoint = boilingPoint + BP_MP_DOWN_RANGE;
		final int maxBoilingPoint = boilingPoint + BP_MP_UP_RANGE ;

		int removed = 0;
		final Iterator<Unknown> unknownIter = m_activeUnknownsList.iterator();
		while (unknownIter.hasNext()) {
			final Unknown curUnknown = unknownIter.next();
			boolean keepCur = true;

			if (curUnknown.hasBoilingPoint()) {
				final int curBoilingPoint = curUnknown.getBoilingPoint();

				// if the current unknown's BP is out of range, remove it
				if (curBoilingPoint < minBoilingPoint
						|| curBoilingPoint > maxBoilingPoint) {
					keepCur = false;
				}
			} else {

				// if the current unknown has no BP, remove it
				keepCur = false;
			}

			// if we're keeping the current unknown, go to the next one
			if (keepCur) {
				continue;
			}

			unknownIter.remove();
			removed++;
		}

		Log.writeLine("Removed " + removed + " unknowns outside BP range.");
		Log.writeLine(" [" + minBoilingPoint + ", " + maxBoilingPoint + "] {"
				+ m_selectedUnknown.getBoilingPoint() + "}.", DebugLevel.MOST);
	}

	/**
	 * Remove all unknowns outside the acceptable range of melting points.
	 */
	public void removeOutsideMeltingPointRange() {
		if (!m_selectedUnknown.hasMeltingPoint()) {
			return;
		}

		final int meltingPoint = m_selectedUnknown.getMeltingPoint();
		final int minMeltingPoint = meltingPoint + BP_MP_DOWN_RANGE;
		final int maxMeltingPoint = meltingPoint + BP_MP_UP_RANGE;

		int removed = 0;
		final Iterator<Unknown> unknownIter = m_activeUnknownsList.iterator();
		while (unknownIter.hasNext()) {
			final Unknown curUnknown = unknownIter.next();
			boolean keepCur = true;

			if (curUnknown.hasMeltingPoint()) {
				final int curMeltingPoint = curUnknown.getMeltingPoint();

				// if the current unknown's MP is out of range, remove it
				if (curMeltingPoint < minMeltingPoint
						|| curMeltingPoint > maxMeltingPoint) {
					keepCur = false;
				}
			} else {

				// if the current unknown has no MP, remove it
				keepCur = false;
			}

			// if we're keeping the current unknown, go to the next one
			if (keepCur) {
				continue;
			}

			unknownIter.remove();
			removed++;
		}

		Log.writeLine("Removed " + removed + " unknowns outside MP range.");
		Log.writeLine(" [" + minMeltingPoint + ", " + maxMeltingPoint + "] {"
				+ m_selectedUnknown.getMeltingPoint() + "}.", DebugLevel.MOST);
	}

	/**
	 * Run the agent.
	 */
	public void run() {
		int testCount = 0;
		final ArrayList<Unknown> failedUnknownList = new ArrayList<Unknown>();
		boolean removedForBpMp = false;
		boolean findSuccess = false;
		final int numUnknowns = m_activeUnknownsList.size();
		final float hundredDivByNumUnknowns = 100.0f / (float)numUnknowns;
		final int numTotal = numUnknowns / UNKNOWN_INCREMENT;

		do {
			// TODO remove this to speed up (but have to wait for end to get log output)
			Log.flush();

			final float numDone = m_activeUnknownsList.indexOf(m_selectedUnknown);
			final float percentDone = numDone * hundredDivByNumUnknowns;
			System.out.println(m_shortFormatter.format(percentDone) + "% completed : Unknown #"
					+ m_selectedUnknown.getId());

			Log.writeLine("=========================================================\n"
							+ "BEGIN\n"
							+ "=========================================================\n"
							+ "Selected unknown: "
							+ m_selectedUnknown.getFullName(),
							DebugLevel.MINIMAL);

			testCount = 0;
			removedForBpMp = false;

			do {
				findSuccess = true;

				updateActiveClasses();

				// before we start considering derivative tests (when there is
				// only one unique class
				// remaining), we remove unknowns outside boiling/melting point
				// range
				if (!removedForBpMp && m_activeUnknownsList.size() > 1
						&& m_activeClassSet.size() == 1) {
					Log.writeLine("\n===================\nCONSIDERING BP/MP"
							+ "\n===================", DebugLevel.MINIMAL);
					removeOutsideBoilingPointRange();
					removeOutsideMeltingPointRange();
					removedForBpMp = true;

					continue;
				}

				testCount++;
				Log.writeLine("\n===================\nTEST #" + testCount
						+ "\n===================", DebugLevel.MINIMAL);
				m_resultsTable = "\n==================\nResults for Test #"
						+ testCount
						+ "\n"
						+ "==================\n"
						+ "=====\t=====\t=====\t=====\t\t=====\t\t=====\t\t=====\t\t=====\t\t=====\n"
						+ "WGT  \tWGT F\tTIME \tTP   \t\tCF   \t\tCF-SC\t\tDF   \t\tDF-SC\t\tTest \n"
						+ "=====\t=====\t=====\t=====\t\t=====\t\t=====\t\t=====\t\t=====\t\t=====\n";

				Log.writeLine(m_activeUnknownsList.size() + " unknowns and "
						+ m_activeTestsList.size() + " tests with "
						+ m_activeClassSet.size() + " classes remain.");

				Log.write("Classes: ");
				for (final String curClass : m_activeClassSet) {
					Log.write(curClass + ", ");
				}
				Log.writeLine();

				// recalculate and display time penalties
				calculateTimePenalties();

				if (m_activeTestsList.size() == 0) {
					Log.writeLine("No more tests to execute.");
					findSuccess = false;
					break;
				}

				// get the best test at this juncture
				final Test currentBestTest = selectBestTest();

				// if there's no best test, break out
				if (currentBestTest == null) {
					Log.writeLine("No best test.");
					findSuccess = false;
					
					break;
				}
				
				Log.writeLine("\tBest test is " + currentBestTest.getFullName(), DebugLevel.MINIMAL);					

				// run the test to get result
				final TestResult expectedResult = m_selectedUnknown.getResult(currentBestTest);

				Log.write("Running " + currentBestTest.getFullName() + " on "
						+ m_selectedUnknown.getFullName() + " results in\n\t");

				if(expectedResult.getResult() == Unknown.NEGATIVE_RESULT) {
					Log.writeLine("\tNEGATIVE");
				} else if (currentBestTest.isDerivative()) {
					Log.writeLine("derivative melting point of "
							+ (expectedResult == null ? "NULL_RESULT"
									: expectedResult.getResult()));
				} else {
					final String resultString = expectedResult == null ? "NULL"
							: m_dbInterface.getString(expectedResult.getResult());
					Log.writeLine("'" + resultString + "'");
				}

				// remove unknowns that don't match the result
				int numRemoved = 0;
				final Iterator<Unknown> unknownIter = m_activeUnknownsList.iterator();
				while (unknownIter.hasNext()) {
					if (unknownIter.next().getResult(currentBestTest) != expectedResult) {
						unknownIter.remove();
						numRemoved++;
					}
				}

				Log.writeLine("Removed " + numRemoved + " unknowns not matching result "
						+ (expectedResult == null ? "null" : expectedResult.getResult()) + ".");

				// if we didn't remove anything, we didn't have a best test
				if (numRemoved == 0) {
					Log.writeLine("Did not eliminate any unknowns. Aborting.");
					findSuccess = false;
					
					// force a dump of the table
					Log.writeLine(m_resultsTable, DebugLevel.MINIMAL);
					
					break;
				}

				m_activeTestsList.remove(currentBestTest);

				Log.writeLine("Removed " + currentBestTest.getFullName()
						+ " from available tests.");
				
				// force a dump of the table if test isn't solubility and CF and DF are zero
				if(
					(!currentBestTest.getName().trim().toLowerCase().equals("solubility") 
					&& currentBestTest.getClassFactorScore() == 0.0f 
					&& currentBestTest.getDiscrimFactorScore() == 0.0f)
					|| expectedResult == null) {
						Log.writeLine(m_resultsTable, DebugLevel.MINIMAL);
				} else {
					Log.writeLine(m_resultsTable, DebugLevel.MORE);
				}
			} while (m_activeUnknownsList.size() > 1);

			Log.writeLine("\n===================\nRESULT"
					+ "\n===================");

			if (m_activeUnknownsList.size() == 0) {
				Log.writeLine("***ERROR: sequence of tests resulted in zero active unknowns.",
								DebugLevel.MINIMAL);
			} else if (m_activeUnknownsList.size() > 1) {
				if(m_activeTestsList.size() == 0) {
					Log.writeLine("No remaining active tests.");
				}
				
				Log.writeLine("The unknown could be any of the following:",
								DebugLevel.MINIMAL);

				for (final Unknown curUnknown : m_activeUnknownsList) {
					Log.writeLine(curUnknown.getFullName());
				}
			} else {
				Log.writeLine("Discovered unknown "
						+ m_activeUnknownsList.get(0).getFullName() + " using "
						+ testCount + " tests.", DebugLevel.MINIMAL);
			}

			// if testing more than one, track number succeeded/failed
			if (m_mode == Mode.INCREMENT) {
				if (findSuccess) {
					Log.writeLine("[SUCCESS!]");
				} else {
					failedUnknownList.add(m_selectedUnknown);
					Log.writeLine("[FAILED]");
				}

				Log.writeLine("");
			}
		} while (initialize());

		// if testing more than one, report number succeeded/failed
		if (m_mode == Mode.INCREMENT) {
			final int numFailed = failedUnknownList.size();
			Log.writeLine("*** " + numFailed + " out of " + numTotal + " failed. ***",
					DebugLevel.MINIMAL);

			Log.write("Failed unknowns: ");
			for (final Unknown curUnknown : failedUnknownList) {
				Log.write(curUnknown.getFullName() + ", ");
			}
			Log.writeLine();
		}
	}

	/**
	 * Find the best test given the current state.
	 * 
	 * @return The best test.
	 */

	// if we're only considering class tests, ONLY CHECK THE CLASS TEST TO CALC
	// DF
	private Test selectBestTest() {

		// if we have solubility, that's automatically the best test
		for (final Test curTest : m_activeTestsList) {
			if (curTest.getName().toLowerCase().trim().equals("solubility")) {
				return curTest;
			}
		}

		boolean multipleClassesRemain = true;
		ArrayList<Test> testsToConsiderList = new ArrayList<Test>();

		// if we have more than one class left, we only consider classification
		// tests
		if (m_activeClassSet.size() > 1) {
			Log.writeLine("Multiple classes remain.");

			multipleClassesRemain = true;

			for (final Test curTest : m_activeTestsList) {
				TestResult result = m_selectedUnknown.getResult(curTest); 
				if(result != null) {
					if (!curTest.isDerivative()) {
						testsToConsiderList.add(curTest);
					}
				}
			}
		} else { // with only 1 class left, consider all tests
			Log.writeLine("Considering derivative tests.");
			
			//testsToConsiderList.addAll(m_activeTestsList);
			
			// only include tests which have a result on the selected unknown
			//*
			for (final Test curTest : m_activeTestsList) {
				TestResult result = m_selectedUnknown.getResult(curTest); 
				if(result != null) {
					testsToConsiderList.add(curTest);
				}
			}
			//*/
		}
		
		// no tests to consider => no best test
		if(testsToConsiderList.size() == 0) return null;
		
		// start by assuming best test is first test
		Test bestTest = testsToConsiderList.get(0);

		// find the best test
		for (final Test curTest : testsToConsiderList) {

			//
			// calculate CF for classification tests
			//

			if (curTest.isDerivative()) {
				curTest.setClassFactor(0);
				curTest.setClassFactorScore(0);
			} else {
				final float classFactor = calculateClassFactor(curTest);
				curTest.setClassFactor(classFactor);

				// calculate CF score
				final float curCfScore = convertToScore(curTest, classFactor);
				curTest.setClassFactorScore(curCfScore);
			}

			//
			// calculate DF for all tests
			//

			float discrimFactor = 0.0f;

			// for derivative tests, use range to computer DF
			if (curTest.isDerivative()) {
				discrimFactor = calculateDiscrimFactor(curTest, DERIV_DF_DOWN_RANGE, 
						DERIV_DF_UP_RANGE);
			} else {
				discrimFactor = calculateDiscrimFactor(curTest, 0.0f, 0.0f);
			}
			curTest.setDiscrimFactor(discrimFactor);

			final float curDfScore = convertToScore(curTest, discrimFactor);
			curTest.setDiscrimFactorScore(curDfScore);

			final float bestDfScore = (bestTest == null) ? Float.NEGATIVE_INFINITY
					: bestTest.getDiscrimFactorScore();

			// if we have more than one class, use CF
			if (multipleClassesRemain) {
				// if the current best test is derivative, make sure we switch
				// to classification
				if (bestTest.isDerivative()) {
					bestTest = curTest;
				} else {

					final float bestCfScore = (bestTest == null) ? Float.NEGATIVE_INFINITY
							: bestTest.getClassFactorScore();
					final float cfScore = curTest.getClassFactorScore();

					// higher score "wins"
					if (cfScore > bestCfScore) {
						bestTest = curTest;
					} else if (cfScore == bestCfScore) {

						// use DF as a tie-breaker
						if (curDfScore >= bestDfScore) {
							bestTest = curTest;
						}
					}
				}
			} else { // if there is only one class left, use DF
				if (curDfScore >= bestDfScore) {
					bestTest = curTest;
				}
			}
		}

		// sort remaining tests and construct the results table
		Collections.sort(testsToConsiderList);
		for (final Test curTest : testsToConsiderList) {
			m_resultsTable += curTest.getAllDataAsTableRow();
		}

		// best test has the highest DF / CF
		Log.writeLine("With DF score "
				+ m_formatter.format(bestTest.getDiscrimFactorScore())
				+ " and CF score "
				+ m_formatter.format(bestTest.getClassFactorScore()));

		return bestTest;
	}

	/**
	 * Select an unknown based on the current mode. Random selects a random
	 * unknown, Select allows input to select an unknown (todo), and TestAll
	 * selects the next unknown on the list.
	 */
	private boolean selectUnknown() {

		// select a random unknown
		if (m_mode == Mode.RANDOM) {
			Log.writeLine("Selecting a random unknown.", Log.DebugLevel.MOST);			

			// only testing one unknown
			if (m_selectedUnknown != null) {
				return false;
			}

			final Random rand = new Random();
			final int unknownIndex = rand.nextInt(m_activeUnknownsList.size());
			m_selectedUnknown = m_activeUnknownsList.get(unknownIndex);
		} else {
			Log.writeLine("Selecting the next unknown.", Log.DebugLevel.MOST);
			
			if (m_selectedUnknown == null) {
				for(Unknown curUnknown : m_activeUnknownsList) {
					if(curUnknown.getId() == 1) {
						m_selectedUnknown = curUnknown;
						return true;
					}
				}
			} else {
				final int oldIndex = m_selectedUnknown.getId();
				final int newIndex = oldIndex + UNKNOWN_INCREMENT;

				if (newIndex > m_activeUnknownsList.size()) {
					return false;
				} else {
					for(Unknown curUnknown : m_activeUnknownsList) {
						if(curUnknown.getId() == newIndex) {
							m_selectedUnknown = curUnknown;
							return true;
						}
					}
				}
			}
		}

		return true;
	}

	/**
	 * Update classifications list for the remaining unknowns.
	 */
	private void updateActiveClasses() {
		m_activeClassSet.clear();

		for (final Unknown unknown : m_activeUnknownsList) {
			m_activeClassSet.add(unknown.getUnknownClass());
		}
	}
}
